luci-proto-openconnect: bug fixes for cert read and write methods
authorPaul Donald <[email protected]>
Fri, 25 Jul 2025 18:55:49 +0000 (20:55 +0200)
committerPaul Donald <[email protected]>
Fri, 25 Jul 2025 18:55:49 +0000 (20:55 +0200)
follow-up to: aa955d6465b4d0f00cc713904e2de7bfb0cbd062

Minor refactor of ucode, and some GUI fixes to ensure certificates are
written properly.

Signed-off-by: Paul Donald <[email protected]>
protocols/luci-proto-openconnect/htdocs/luci-static/resources/protocol/openconnect.js
protocols/luci-proto-openconnect/root/usr/share/rpcd/ucode/luci.openconnect

index 05c559ce84d6611772999f86ba7fa7e7d224101d..86ff009029dda816946a6e237d43e6f057b33e6f 100644 (file)
@@ -4,14 +4,14 @@
 'require network';
 'require validation';
 
-var callGetCertificateFiles = rpc.declare({
+const callGetCertificateFiles = rpc.declare({
        object: 'luci.openconnect',
        method: 'getCertificates',
        params: [ 'interface' ],
        expect: { '': {} }
 });
 
-var callSetCertificateFiles = rpc.declare({
+const callSetCertificateFiles = rpc.declare({
        object: 'luci.openconnect',
        method: 'setCertificates',
        params: [ 'interface', 'user_certificate', 'user_privatekey', 'ca_certificate' ],
@@ -22,14 +22,14 @@ network.registerPatternVirtual(/^vpn-.+$/);
 
 function sanitizeCert(s) {
        if (typeof(s) != 'string')
-               return null;
+               return '';
 
        s = s.trim();
 
        if (s == '')
-               return null;
+               return s;
 
-       s = s.replace(/\r\n?/g, '\n');
+       s = s.replace(/\r?\n/g, '\n');
 
        if (!s.match(/\n$/))
                s += '\n';
@@ -38,24 +38,22 @@ function sanitizeCert(s) {
 }
 
 function validateCert(priv, section_id, value) {
-       var beg = priv ? /^-----BEGIN (RSA )?PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
-           end = priv ? /^-----END (RSA )?PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
-           lines = value.trim().split(/[\r\n]/),
-           start = false,
-           i;
-
-       if (value === null || value === '')
+       if (!value?.trim())
                return true;
 
-       for (i = 0; i < lines.length; i++) {
-               if (lines[i].match(beg))
-                       start = true;
-               else if (start && !lines[i].match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/))
-                       break;
-       }
+       const beg = priv ? /^-----BEGIN (RSA )?PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/;
+       const end = priv ? /^-----END (RSA )?PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/;
+       const lines = value.trim().split(/[\r?\n]/);
+       const base64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
+       const errmsg = _('This does not look like a valid PEM file');
+
+
+       if (!lines?.[0].match(beg) || !lines.at(-1).match(end))
+               return errmsg;
 
-       if (!start || i < lines.length - 1 || !lines[i].match(end))
-               return _('This does not look like a valid PEM file');
+       for (let i = 1; i < lines.length - 1; i++)
+               if (!base64.test(lines[i]))
+                       return errmsg;
 
        return true;
 }
@@ -90,9 +88,9 @@ return network.registerProtocol('openconnect', {
        },
 
        renderFormOptions: function(s) {
-               var dev = this.getDevice().getName(),
-                   certLoadPromise = null,
-                   o;
+               const dev = this.getDevice().getName();
+               let certLoadPromise = null;
+               let o;
 
                o = s.taboption('general', form.ListValue, 'vpn_protocol', _('VPN Protocol'));
                o.value('anyconnect', 'OpenConnect or Cisco AnyConnect SSL VPN');
@@ -106,7 +104,7 @@ return network.registerProtocol('openconnect', {
                o = s.taboption('general', form.Value, 'uri', _('VPN Server'));
                o.placeholder = 'https://example.com:443/usergroup';
                o.validate = function(section_id, value) {
-                       var m = String(value).match(/^(?:(\w+):\/\/|)(?:\[([0-9a-f:.]{2,45})\]|([^\/:]+))(?::([0-9]{1,5}))?(?:\/.*)?$/i);
+                       const m = String(value).match(/^(?:(\w+):\/\/|)(?:\[([0-9a-f:.]{2,45})\]|([^\/:]+))(?::([0-9]{1,5}))?(?:\/.*)?$/i);
 
                        if (!m)
                                return _('Invalid server URL');
@@ -163,7 +161,7 @@ return network.registerProtocol('openconnect', {
                        return certLoadPromise.then(function(certs) { return certs.user_certificate });
                };
                o.write = function(section_id, value) {
-                       return callSetCertificateFiles(section_id, sanitizeCert(value), null, null);
+                       return callSetCertificateFiles(section_id, sanitizeCert(value), '', '');
                };
 
                o = s.taboption('general', form.TextValue, 'userkey', _('User key (PEM encoded)'));
@@ -175,7 +173,7 @@ return network.registerProtocol('openconnect', {
                        return certLoadPromise.then(function(certs) { return certs.user_privatekey });
                };
                o.write = function(section_id, value) {
-                       return callSetCertificateFiles(section_id, null, sanitizeCert(value), null);
+                       return callSetCertificateFiles(section_id, '', sanitizeCert(value), '');
                };
 
                o = s.taboption('general', form.TextValue, 'ca', _('CA certificate; if empty it will be saved after the first connection.'));
@@ -187,7 +185,7 @@ return network.registerProtocol('openconnect', {
                        return certLoadPromise.then(function(certs) { return certs.ca_certificate });
                };
                o.write = function(section_id, value) {
-                       return callSetCertificateFiles(section_id, null, null, sanitizeCert(value));
+                       return callSetCertificateFiles(section_id, '', '', sanitizeCert(value));
                };
 
                o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));
index 8e728e6bd00ed262d2dbb50f97fb211118232c19..1bc13a17068aaba4f30e58e3c76d9e7ca955c931 100644 (file)
@@ -1,41 +1,32 @@
-#!/usr/bin/env ucode
-
 'use strict';
 
 import { readfile, writefile, stat } from 'fs';
 
 const interfaceregex = /^[a-zA-Z0-9_]+$/;
-const user_certificate_string = "/etc/openconnect/user-cert-vpn-%s.pem";
-const user_privatekey_string = "/etc/openconnect/user-key-vpn-%s.pem";
-const ca_certificate_string = "/etc/openconnect/ca-vpn-%s.pem";
-
+const paths = {
+       user_certificate: "/etc/openconnect/user-cert-vpn-%s.pem",
+       user_privatekey:  "/etc/openconnect/user-key-vpn-%s.pem",
+       ca_certificate:   "/etc/openconnect/ca-vpn-%s.pem"
+};
 
-// Utility to read a file
 function _readfile(path) {
-       let _stat = stat(path);
-       if (_stat && _stat.type == "file") {
-               let content = readfile(path);
-               return content ? trim(content) : 'File empty';
-       }
-       return 'File not found';
+       let s = stat(path);
+       return (s?.type == 'file') ? trim(readfile(path) ?? '') || 'File empty' : null;
 }
 
-// Utility to write a file
 function _writefile(path, data) {
-       if (!data) {
-               return false;
-       }
-       return writefile(path, data) == length(data);
+       return data ? writefile(path, data) == length(data) : false;
 }
 
-const methods = {
+function is_valid_iface(ifname) {
+       return ifname && match(ifname, interfaceregex);
+}
 
-       list:{
+const methods = {
+       list: {
                call: function() {
                        return {
-                               getCertificates: {
-                                       interface: "interface"
-                               },
+                               getCertificates: { interface: "interface" },
                                setCertificates: {
                                        interface: "interface",
                                        user_certificate: "user_certificate",
@@ -47,29 +38,16 @@ const methods = {
        },
 
        getCertificates: {
-               args: {
-                       interface: "interface",
-               },
+               args: { interface: "interface" },
                call: function(req) {
+                       let iface = req.args?.interface;
+                       if (!is_valid_iface(iface)) return;
 
-                       const _interface = req.args?.interface;
-                       if (!_interface || !match(_interface, interfaceregex)) {
-                               // printf("Invalid interface name");
-                               return;
-                       }
-
-                       const user_certificate_pem = _readfile(sprintf(user_certificate_string, _interface));
-                       const user_privatekey_pem = _readfile(sprintf(user_privatekey_string, _interface));
-                       const ca_certificate_pem = _readfile(sprintf(ca_certificate_string, _interface));
-
-                       if(user_certificate_pem && user_privatekey_pem && ca_certificate_pem){
-                               return {
-                                       user_certificate: user_certificate_pem,
-                                       user_privatekey: user_privatekey_pem,
-                                       ca_certificate: ca_certificate_pem,
-                               };
-                       }
+                       let result = {};
+                       for (let k in paths)
+                               result[k] = _readfile(sprintf(paths[k], iface));
 
+                       return result;
                }
        },
 
@@ -81,35 +59,17 @@ const methods = {
                        ca_certificate: "ca_certificate",
                },
                call: function(req) {
+                       let iface = req.args?.interface;
+                       if (!is_valid_iface(iface)) return;
 
                        let result = false;
-                       let _interface = req.args?.interface;
-
-                       if (!_interface || !match(_interface, interfaceregex)) {
-                               // printf("Invalid interface name");
-                               return;
-                       }
-
-                       /* the interface is set up to call 1 write per certificate,
-                       with only one of the following arguments not null */
-                       if (req.args?.user_certificate) {
-                               result = _writefile(sprintf(user_certificate_string, _interface), req.args?.user_certificate);
-                       }
-                       if (req.args?.user_privatekey) {
-                               result = _writefile(sprintf(user_privatekey_string, _interface), req.args?.user_privatekey);
-                       }
-                       if (req.args?.ca_certificate) {
-                               result = _writefile(sprintf(ca_certificate_string, _interface), req.args?.ca_certificate);
+                       for (let k in paths) {
+                               if (req.args?.[k])
+                                       result = _writefile(sprintf(paths[k], iface), req.args[k]);
                        }
-
-                       return {
-                               result: result,
-                       };
-
+                       return { result: result };
                }
        }
-
 };
 
 return { 'luci.openconnect': methods };
-